package com.atom.plugin.core

import com.android.build.api.transform.*
import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.LibraryExtension
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils.delete
import com.android.utils.FileUtils.deletePath
import com.atom.plugin.core.concurrent.Schedulers
import com.atom.plugin.core.utils.FileUtils
import com.atom.plugin.core.utils.IOUtils
import com.atom.plugin.core.utils.ZipUtils
import com.google.common.collect.ImmutableList
import com.google.common.collect.Sets
import com.google.common.hash.Hashing
import com.sun.org.apache.xpath.internal.operations.Bool
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionContainer
import org.gradle.api.plugins.PluginContainer
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.util.CheckClassAdapter
import java.io.*
import java.net.MalformedURLException
import java.net.URL
import java.net.URLClassLoader
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.zip.ZipEntry
import java.util.zip.ZipException
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream

abstract class CorePlugin<E : AbstractExtension> : Transform(), Plugin<Project> {

    companion object {
        @Suppress("DEPRECATION")
        fun getUniqueJarName(jarFile: File): String {
            val origJarName = jarFile.name
            val hashing = Hashing.sha1().hashString(jarFile.path, Charsets.UTF_16LE).toString()
            val dotPos = origJarName.lastIndexOf('.')
            return if (dotPos < 0) {
                String.format("%s_%s", origJarName, hashing)
            } else {
                val nameWithoutDotExt = origJarName.substring(0, dotPos)
                val dotExt = origJarName.substring(dotPos)
                String.format("%s_%s%s", nameWithoutDotExt, hashing, dotExt)
            }
        }

        fun appendSuffix(jarFile: File, suffix: String): String {
            val origJarName = jarFile.name
            val dotPos = origJarName.lastIndexOf('.')
            return if (dotPos < 0) {
                String.format("%s_%s", origJarName, suffix)
            } else {
                val nameWithoutDotExt = origJarName.substring(0, dotPos)
                val dotExt = origJarName.substring(dotPos)
                String.format("%s_%s%s", nameWithoutDotExt, suffix, dotExt)
            }
        }

    }

    protected lateinit var project: Project
    protected lateinit var extension: E

    private var isApp: Boolean = true

    abstract fun getExtensionName(): String

    abstract fun getExtensionClass(): Class<E>

    override fun apply(project: Project) {
        Log.init(project)
        Log.e("Apply ${getExtensionName()} \nDate ${Date()}")
        this.project = project
        val plugins: PluginContainer = project.plugins
        // 主要是负责检查当前的plugin是否在android环境使用
        val hasAppPlugin: Boolean = plugins.hasPlugin(AppPlugin::class.java)
        hasAppPlugin.isTrue {
            val extensions: ExtensionContainer = project.extensions
            val appExtension = extensions.getByType(BaseExtension::class.java)
            this.isApp = appExtension is AppExtension
            this.extension = extensions.create(getExtensionName(), getExtensionClass())
                ?: this.getExtensionClass().newInstance()
            appExtension.registerTransform(this)
            project.afterEvaluate { p ->
                if (appExtension is AppExtension) {
                    afterEvaluate(p, appExtension)
                }
            }
        }
    }

    protected open fun afterEvaluate(project: Project, app: AppExtension) {
        Log.e("AfterEvaluate ${getExtensionName()} \njava version = ${System.getProperty("java.version")} \nextension = ${extension}")
    }

    override fun getName(): String = getExtensionName()

    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> =
        TransformManager.CONTENT_CLASS

    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return if (isApp) {
            TransformManager.SCOPE_FULL_PROJECT
        } else {
            Sets.immutableEnumSet(QualifiedContent.Scope.PROJECT)
        }
    }

    override fun isIncremental(): Boolean = true

    override fun transform(transformInvocation: TransformInvocation?) {
        super.transform(transformInvocation)
        if (transformInvocation == null) {
            return
        }
        this.extension.also { e ->
            Log.setEnable(e.enableLog)
            Log.setAbort(e.abortError)
            try {
                val start = System.currentTimeMillis()
                Log.w("┌──────────────────────────BeforeTransform──────────────────────────┐")
                Log.w("├ ProjectName  : ${project.name}")
                Log.w("├ ExtensionName: ${getExtensionName()}")
                Log.w("├ ProjectPath  : ${transformInvocation.context.path}")
                Log.w("├ BuildType    : ${transformInvocation.context.variantName}")
                Log.w("├ Incremental  : ${transformInvocation.isIncremental}")
                Log.w("├ Extension    : $e")
                Log.w("├ StartTime    : ${Date(start)}")
                Log.w("└───────────────────────────────────────────────────────────────────┘")
                if (e.enableUse) {
                    runTransform(transformInvocation)
                } else {
                    transparent(transformInvocation)
                }
                val end = System.currentTimeMillis()
                Log.w("┌───────────────────────────AfterTransform──────────────────────────┐")
                Log.w("├ EndTime      : ${Date(end)}")
                Log.w("├ ConsumeTime  : ${(end - start)}ms")
                Log.w("└───────────────────────────────────────────────────────────────────┘")
            } catch (e: Throwable) {
                Log.e("transform error" + e.localizedMessage)
                e.printStackTrace()
                Log.fatal(e)
            }
        }
    }

    private fun runTransform(transformInvocation: TransformInvocation) {
        val isIncremental = transformInvocation.isIncremental
        //如果非增量，则清空旧的输出内容
        if (!isIncremental) {
            try {
                transformInvocation.outputProvider.deleteAll()
            } catch (e: Exception) {
                Log.e("|----runTransform runTransform Init Error=[${e.localizedMessage}]")
                Log.fatal(e)
            }
        }
        val changedFiles = ConcurrentHashMap<File, Status>()
        val inputToOutput = ConcurrentHashMap<File, File>()
        val inputFiles = ArrayList<File>()
        var transformDirectory: File? = null
        transformInvocation.inputs.forEach { transformInput ->
            Log.e("----jarInputs[${transformInput.jarInputs.size}]----------------------------")
            transformInput.jarInputs.forEach { jarInput ->
                // Matrix
                val inputFile = jarInput.file
                changedFiles[inputFile] = jarInput.status
                inputFiles.add(inputFile)
                val outputJar = transformInvocation.outputProvider.getContentLocation(
                    jarInput.name,
                    jarInput.contentTypes,
                    jarInput.scopes,
                    Format.JAR
                )

                inputToOutput[inputFile] = outputJar
                if (transformDirectory == null) transformDirectory = outputJar.parentFile
            }
            Log.e("----directoryInputs[${transformInput.directoryInputs.size}]----------------------------")
            transformInput.directoryInputs.forEach { directoryInput ->
                // Matrix
                changedFiles.putAll(directoryInput.changedFiles)
                val inputDir = directoryInput.file
                inputFiles.add(inputDir)
                val outputDirectory = transformInvocation.outputProvider.getContentLocation(
                    directoryInput.name,
                    directoryInput.contentTypes,
                    directoryInput.scopes,
                    Format.DIRECTORY
                )

                inputToOutput[inputDir] = outputDirectory
                if (transformDirectory == null) transformDirectory = outputDirectory.parentFile
            }
        }

        if (inputFiles.size == 0 || transformDirectory == null) {
            Log.e("Plugin Core trace do not find any input files")
            return
        }
        // Get transform root dir.
        val outputDirectory = transformDirectory!!

        /**
         * step 1
         */
        var start = System.currentTimeMillis()
        val futures = LinkedList<Future<*>>()
        val executor = Schedulers.io()
        val dirInputOutMap = ConcurrentHashMap<File, File>()
        val jarInputOutMap = ConcurrentHashMap<File, File>()
        for (file in inputFiles) {
            if (file.isDirectory) {
                futures.add(
                    executor.submit(
                        CollectDirectoryInputTask(
                            directoryInput = file,
                            mapOfChangedFiles = changedFiles,
                            mapOfInputToOutput = inputToOutput,
                            isIncremental = isIncremental,
                            traceClassDirectoryOutput = outputDirectory,
                            // result
                            resultOfDirInputToOut = dirInputOutMap
                        )
                    )
                )
            } else {
                val status = Status.CHANGED
                futures.add(
                    executor.submit(
                        CollectJarInputTask(
                            inputJar = file,
                            inputJarStatus = status,
                            inputToOutput = inputToOutput,
                            isIncremental = isIncremental,
                            traceClassFileOutput = outputDirectory,
                            uniqueOutputName = true,
                            // result
                            resultOfDirInputToOut = dirInputOutMap,
                            resultOfJarInputToOut = jarInputOutMap
                        )
                    )
                )
            }
        }
        for (future in futures) {
            future.get()
        }
        futures.clear()
        Log.i(
            String.format(
                "[doTransform] Step(1)[Parse]... cost:%sms",
                System.currentTimeMillis() - start
            )
        )
        /**
         * step 2 进行初步的srcInputFile的遍历,主要是进行检查input file中是否存在需要提前采集的内容
         */
        start = System.currentTimeMillis()
        for (srcFile in dirInputOutMap.keys()) {
            val classFileList = ArrayList<File>()
            if (srcFile.isDirectory) {
                listClassFiles(classFileList, srcFile)
            } else {
                classFileList.add(srcFile)
            }
            for (classFile in classFileList) {
                futures.add(executor.submit {
                    var input: InputStream? = null
                    try {
                        input = FileInputStream(classFile)
                        collectClassStream(input, false)
                    } catch (e: Exception) {
                        e.printStackTrace()
                    } finally {
                        try {
                            input?.close()
                        } catch (e: Exception) {

                        }
                    }
                })
            }
        }

        for (jarFile in jarInputOutMap.keys()) {
            futures.add(executor.submit {
                var zipFile: ZipFile? = null
                try {
                    zipFile = ZipFile(jarFile)
                    val enumeration = zipFile.entries()
                    while (enumeration.hasMoreElements()) {
                        val zipEntry = enumeration.nextElement()
                        val zipEntryName = zipEntry.name
                        if (isNeedTraceFile(zipEntryName, jarFile.absolutePath, true)) {
                            var input: InputStream? = null
                            try {
                                input = zipFile.getInputStream(zipEntry) ?: continue
                                // TODO 交给子类去实现
                                collectClassStream(input!!, true)
                            } catch (e: Exception) {
                                e.printStackTrace()
                            } finally {
                                try {
                                    input?.close()
                                } catch (e: Exception) {
                                    Log.e(
                                        String.format(
                                            "close stream err! fromJar :%s fromClass :%s",
                                            jarFile.absolutePath, zipEntryName
                                        )
                                    )
                                }
                            }
                        }
                    }
                } catch (e: Exception) {
                    e.printStackTrace()
                } finally {
                    try {
                        zipFile?.close()
                    } catch (e: Exception) {
                        Log.e(
                            String.format(
                                "close stream err! fromJar:%s",
                                jarFile.absolutePath
                            )
                        )
                    }
                }
            })
        }

        for (future in futures) {
            future.get()
        }
        futures.clear()
        Log.i(
            String.format(
                "[doCollect] Step(2)[Parse]... cost:%sms",
                System.currentTimeMillis() - start
            )
        )
        /**
         * step 3 进行class文件的变换插入删除等操作
         */
        start = System.currentTimeMillis()
        val allInputs = ArrayList<File>().also {
            it.addAll(dirInputOutMap.keys)
            it.addAll(jarInputOutMap.keys)
        }
        val traceClassLoader = getClassLoader(project, allInputs)
        var traceError = false
        for ((input, output) in dirInputOutMap.entries) {
            futures.add(executor.submit {
                val classFileList = ArrayList<File>()
                if (input.isDirectory) {
                    listClassFiles(classFileList, input)
                } else {
                    classFileList.add(input)
                }

                for (classFile in classFileList) {
                    var `is`: InputStream? = null
                    var os: FileOutputStream? = null
                    try {
                        val changedFileInputFullPath = classFile.absolutePath
                        val changedFileOutput = File(
                            changedFileInputFullPath.replace(
                                input.absolutePath,
                                output.absolutePath
                            )
                        )
                        if (changedFileOutput.canonicalPath == classFile.canonicalPath) {
                            throw RuntimeException("Input file(" + classFile.canonicalPath + ") should not be same with output!")
                        }
                        if (!changedFileOutput.exists()) {
                            changedFileOutput.parentFile.mkdirs()
                        }
                        changedFileOutput.createNewFile()
                        if (isNeedTraceFile(classFile.name, classFile.absolutePath, false)) {
                            `is` = FileInputStream(classFile)
                            val data = transformClassStream(`is`)
                            FileUtils.closeQuietly(`is`)
                            if (extension.checkClass) {
                                try {
                                    val cr = ClassReader(data)
                                    val cw = ClassWriter(0)
                                    val check: ClassVisitor = CheckClassAdapter(cw)
                                    cr.accept(check, ClassReader.EXPAND_FRAMES)
                                } catch (e: Throwable) {
                                    System.err.println("Trace Class Output ERROR: " + e.message + ", " + classFile)
                                    traceError = true
                                }
                            }
                            os = if (output.isDirectory) {
                                FileOutputStream(changedFileOutput)
                            } else {
                                FileOutputStream(output)
                            }
                            os.write(data)
                            os.close()
                        } else {
                            FileUtils.copyFileUsingStream(classFile, changedFileOutput)
                        }
                    } catch (e: java.lang.Exception) {
                        Log.e(
                            String.format(
                                "[innerTraceMethodFromSrc] input:%s e:%s",
                                input.name,
                                e.message
                            )
                        )
                        try {
                            Files.copy(
                                input.toPath(),
                                output.toPath(),
                                StandardCopyOption.REPLACE_EXISTING
                            )
                        } catch (e1: Exception) {
                            e1.printStackTrace()
                        }
                    } finally {
                        try {
                            `is`?.close()
                            os?.close()
                        } catch (e: Exception) {
                            // ignore
                        }
                    }
                }
            })
        }

        for ((input, output) in jarInputOutMap.entries) {
            futures.add(executor.submit {
                var zipOutputStream: ZipOutputStream? = null
                var zipFile: ZipFile? = null
                try {
                    zipOutputStream = ZipOutputStream(FileOutputStream(output))
                    zipFile = ZipFile(input)
                    val enumeration = zipFile!!.entries()
                    while (enumeration.hasMoreElements()) {
                        val zipEntry = enumeration.nextElement()
                        val zipEntryName = zipEntry.name
                        if (ZipUtils.preventZipSlip(output, zipEntryName)) {
                            Log.e(String.format("Unzip entry %s failed!", zipEntryName))
                            continue
                        }
                        if (isNeedTraceFile(zipEntryName, input.absolutePath, true)) {
                            val inputStream = zipFile!!.getInputStream(zipEntry)
                            val data = transformClassStream(inputStream)
                            FileUtils.closeQuietly(inputStream)
                            if (extension.checkClass) {
                                try {
                                    val r = ClassReader(data)
                                    val w = ClassWriter(0)
                                    val v: ClassVisitor = CheckClassAdapter(w)
                                    r.accept(v, ClassReader.EXPAND_FRAMES)
                                } catch (e: Throwable) {
                                    System.err.println("Trace Jar Output ERROR: " + e.message + ", " + zipEntryName)
                                    traceError = true
                                }
                            }
                            val byteArrayInputStream: InputStream = ByteArrayInputStream(data)
                            val newZipEntry = ZipEntry(zipEntryName)
                            FileUtils.addZipEntry(
                                zipOutputStream,
                                newZipEntry,
                                byteArrayInputStream
                            )
                        } else {
                            val inputStream = zipFile!!.getInputStream(zipEntry)
                            val newZipEntry = ZipEntry(zipEntryName)
                            FileUtils.addZipEntry(zipOutputStream, newZipEntry, inputStream)
                        }
                    }
                } catch (e: java.lang.Exception) {
                    Log.e(
                        String.format(
                            "[innerTraceMethodFromJar] input:%s output:%s e:%s",
                            input,
                            output,
                            e.message
                        )
                    )
                    (e as? ZipException)?.printStackTrace()
                    try {
                        if (input.length() > 0) {
                            Files.copy(
                                input.toPath(),
                                output.toPath(),
                                StandardCopyOption.REPLACE_EXISTING
                            )
                        } else {
                            Log.e(
                                String.format(
                                    "[innerTraceMethodFromJar] input:%s is empty",
                                    input
                                )
                            )
                        }
                    } catch (e1: java.lang.Exception) {
                        e1.printStackTrace()
                    }
                } finally {
                    try {
                        if (zipOutputStream != null) {
                            zipOutputStream.finish()
                            zipOutputStream.flush()
                            zipOutputStream.close()
                        }
                        if (zipFile != null) {
                            zipFile!!.close()
                        }
                    } catch (e: java.lang.Exception) {
                        Log.e("close stream err!")
                    }
                }
            })
        }

        for (future in futures) {
            future.get()
        }
        if (traceError) {
            throw IllegalArgumentException("something wrong with trace, see detail log before")
        }
        futures.clear()
        Log.i(
            String.format(
                "[doTransform] Step(3)[Parse]... cost:%sms",
                System.currentTimeMillis() - start
            )
        )
    }

    abstract fun collectClassStream(inputStream: InputStream, isFromJar: Boolean)

    abstract fun transformClassStream(inputStream: InputStream): ByteArray

    class CollectDirectoryInputTask(
        private val directoryInput: File,
        private val mapOfChangedFiles: Map<File, Status>,
        private val mapOfInputToOutput: Map<File, File>,
        private val isIncremental: Boolean,
        private val traceClassDirectoryOutput: File,
        private val resultOfDirInputToOut: MutableMap<File, File>
    ) : Runnable {
        override fun run() {
            try {
                val dirInput = directoryInput
                val dirOutput = if (mapOfInputToOutput.containsKey(dirInput)) {
                    mapOfInputToOutput[dirInput]!!
                } else {
                    File(traceClassDirectoryOutput, dirInput.name)
                }
                val inputFullPath = dirInput.absolutePath
                val outputFullPath = dirOutput.absolutePath

                if (!dirOutput.exists()) {
                    dirOutput.mkdirs()
                }

                if (!dirInput.exists() && dirOutput.exists()) {
                    if (dirOutput.isDirectory) {
                        deletePath(dirOutput)
                    } else {
                        delete(dirOutput)
                    }
                }

                if (isIncremental) {
                    val outChangedFiles = HashMap<File, Status>()

                    for ((changedFileInput, status) in mapOfChangedFiles) {
                        val changedFileInputFullPath = changedFileInput.absolutePath

                        // mapOfChangedFiles is contains all. each collectDirectoryInputTask should handle itself, should not handle other file
                        if (!changedFileInputFullPath.contains(inputFullPath)) {
                            continue
                        }

                        val changedFileOutput =
                            File(changedFileInputFullPath.replace(inputFullPath, outputFullPath))

                        if (status == Status.ADDED || status == Status.CHANGED) {
                            resultOfDirInputToOut[changedFileInput] = changedFileOutput
                        } else if (status == Status.REMOVED) {
                            changedFileOutput.delete()
                        }
                        outChangedFiles[changedFileOutput] = status
                    }
                } else {
                    resultOfDirInputToOut[dirInput] = dirOutput
                }
            } catch (e: Exception) {
                e.printStackTrace()
                Log.e(e.toString())
            }
        }
    }

    class CollectJarInputTask(
        private val inputJar: File,
        private val inputJarStatus: Status,
        private val inputToOutput: Map<File, File>,
        private val isIncremental: Boolean,
        private val traceClassFileOutput: File,
        private val uniqueOutputName: Boolean,
        private val resultOfDirInputToOut: MutableMap<File, File>,
        private val resultOfJarInputToOut: MutableMap<File, File>
    ) : Runnable {
        override fun run() {
            try {
                val jarInput = inputJar
                val jarOutput = if (inputToOutput.containsKey(jarInput)) {
                    inputToOutput[jarInput]!!
                } else {
                    val outputJarName = if (uniqueOutputName)
                        getUniqueJarName(jarInput)
                    else
                        appendSuffix(jarInput, "traced")
                    File(traceClassFileOutput, outputJarName)
                }

                Log.e(
                    String.format(
                        "CollectJarInputTask input %s -> output %s",
                        jarInput,
                        jarOutput
                    )
                )

                if (!isIncremental && jarOutput.exists()) {
                    jarOutput.delete()
                }
                if (!jarOutput.parentFile.exists()) {
                    jarOutput.parentFile.mkdirs()
                }

                if (IOUtils.isRealZipOrJar(jarInput)) {
                    if (isIncremental) {
                        if (inputJarStatus == Status.ADDED || inputJarStatus == Status.CHANGED) {
                            resultOfJarInputToOut[jarInput] = jarOutput
                        } else if (inputJarStatus == Status.REMOVED) {
                            jarOutput.delete()
                        }
                    } else {
                        resultOfJarInputToOut[jarInput] = jarOutput
                    }
                } else {
                    // TODO for wechat
                    Log.e("Special case for WeChat AutoDex. Its rootInput jar file is actually a txt file contains path list.")
                    // Special case for WeChat AutoDex. Its rootInput jar file is actually
                    // a txt file contains path list.
                    jarInput.inputStream().bufferedReader().useLines { lines ->
                        lines.forEach { realJarInputFullPath ->
                            val realJarInput = File(realJarInputFullPath)
                            // dest jar, moved to extra guard intermediate output dir.
                            val realJarOutput =
                                File(traceClassFileOutput, getUniqueJarName(realJarInput))

                            if (realJarInput.exists() && IOUtils.isRealZipOrJar(realJarInput)) {
                                resultOfJarInputToOut[realJarInput] = realJarOutput
                            } else {
                                realJarOutput.delete()
                                if (realJarInput.exists() && realJarInput.isDirectory) {
                                    val realJarOutputDir =
                                        File(traceClassFileOutput, realJarInput.name)
                                    if (!realJarOutput.exists()) {
                                        realJarOutput.mkdirs()
                                    }
                                    resultOfDirInputToOut[realJarInput] = realJarOutputDir
                                }

                            }
                            // write real output full path to the fake jar at rootOutput.
                            jarOutput.outputStream().bufferedWriter().use { bw ->
                                bw.write(realJarOutput.absolutePath)
                                bw.newLine()
                            }
                        }
                    }

                    jarInput.delete() // delete raw inputList
                }
            } catch (e: Exception) {
                e.printStackTrace()
                Log.e(e.toString())
            }
        }
    }

    // use disable
    private fun transparent(invocation: TransformInvocation) {

        val outputProvider = invocation.outputProvider!!

        if (!invocation.isIncremental) {
            outputProvider.deleteAll()
        }

        for (ti in invocation.inputs) {
            for (jarInput in ti.jarInputs) {
                val inputJar = jarInput.file
                val outputJar = outputProvider.getContentLocation(
                    jarInput.name,
                    jarInput.contentTypes,
                    jarInput.scopes,
                    Format.JAR
                )

                if (invocation.isIncremental) {
                    when (jarInput.status) {
                        Status.NOTCHANGED -> {
                        }
                        Status.ADDED, Status.CHANGED -> {
                            copyFileAndMkdirsAsNeed(inputJar, outputJar)
                        }
                        Status.REMOVED -> delete(outputJar)
                        else -> {}
                    }
                } else {
                    copyFileAndMkdirsAsNeed(inputJar, outputJar)
                }
            }
            for (directoryInput in ti.directoryInputs) {
                val inputDir = directoryInput.file
                val outputDir = outputProvider.getContentLocation(
                    directoryInput.name,
                    directoryInput.contentTypes,
                    directoryInput.scopes,
                    Format.DIRECTORY
                )

                if (invocation.isIncremental) {
                    for (entry in directoryInput.changedFiles.entries) {
                        val inputFile = entry.key
                        when (entry.value) {
                            Status.NOTCHANGED -> {
                            }
                            Status.ADDED, Status.CHANGED -> if (!inputFile.isDirectory) {
                                val outputFile = toOutputFile(outputDir, inputDir, inputFile)
                                copyFileAndMkdirsAsNeed(inputFile, outputFile)
                            }
                            Status.REMOVED -> {
                                val outputFile = toOutputFile(outputDir, inputDir, inputFile)
                                com.android.utils.FileUtils.deleteIfExists(outputFile)
                            }
                            else -> {}
                        }
                    }
                } else {
                    for (`in` in com.android.utils.FileUtils.getAllFiles(inputDir)) {
                        val out = toOutputFile(outputDir, inputDir, `in`)
                        copyFileAndMkdirsAsNeed(`in`, out)
                    }
                }
            }
        }
    }

    private fun copyFileAndMkdirsAsNeed(from: File, to: File) {
        if (from.exists()) {
            to.parentFile.mkdirs()
            com.android.utils.FileUtils.copyFile(from, to)
        }
    }

    private fun toOutputFile(outputDir: File, inputDir: File, inputFile: File): File {
        return File(
            outputDir,
            com.android.utils.FileUtils.relativePossiblyNonExistingPath(inputFile, inputDir)
        )
    }

    //Common
    private val UN_TRACE_CLASS = arrayOf("R.class", "R$", "Manifest", "BuildConfig")
    private fun listClassFiles(classFiles: ArrayList<File>, folder: File) {
        val files = folder.listFiles()
        if (null == files) {
            Log.e(String.format("[listClassFiles] files is null! %s", folder.absolutePath))
            return
        }
        for (file in files) {
            if (file == null) {
                continue
            }
            if (file.isDirectory) {
                listClassFiles(classFiles, file)
            } else if (isNeedTraceFile(file.name, file.absolutePath, false)) {
                classFiles.add(file)
            }
        }
    }

    protected open fun isNeedTraceFile(
        fileName: String,
        filePath: String,
        isFromJar: Boolean
    ): Boolean {
        if (fileName.endsWith(".class")) {
            for (unTraceCls in UN_TRACE_CLASS) {
                if (fileName.contains(unTraceCls)) {
                    return false
                }
            }
        } else {
            return false
        }
        return true
    }

    @kotlin.jvm.Throws(MalformedURLException::class)
    private fun getClassLoader(project: Project, inputFiles: Collection<File>): URLClassLoader {
        val urls = ImmutableList.Builder<URL>()
        val androidJar = getAndroidJar(project)
        if (androidJar != null) {
            urls.add(androidJar.toURI().toURL())
        }
        for (inputFile in inputFiles) {
            urls.add(inputFile.toURI().toURL())
        }
        val urlImmutableList = urls.build()
        val classLoaderUrls = urlImmutableList.toTypedArray()
        return URLClassLoader(classLoaderUrls)
    }

    private fun getAndroidJar(project: Project): File? {
        var extension: BaseExtension? = null
        if (project.plugins.hasPlugin("com.android.application")) {
            extension = project.extensions.findByType(AppExtension::class.java)
        } else if (project.plugins.hasPlugin("com.android.library")) {
            extension = project.extensions.findByType(
                LibraryExtension::class.java
            )
        }
        if (extension == null) {
            return null
        }
        var sdkDirectory = extension.sdkDirectory.absolutePath
        val compileSdkVersion = extension.compileSdkVersion
        sdkDirectory = sdkDirectory + File.separator + "platforms" + File.separator
        val androidJarPath = sdkDirectory + compileSdkVersion + File.separator + "android.jar"
        val androidJar = File(androidJarPath)
        return if (androidJar.exists()) androidJar else null
    }
}